Skip to content

[PM-12436] replace node keytar #1012

Open
JaredScar wants to merge 12 commits intorestructure-repofrom
ac/pm-12436-replace-node-keytar-sonnet-attempt
Open

[PM-12436] replace node keytar #1012
JaredScar wants to merge 12 commits intorestructure-repofrom
ac/pm-12436-replace-node-keytar-sonnet-attempt

Conversation

@JaredScar
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-12436

📔 Objective

Rid of node keytar from Directory Connector

renovate bot and others added 11 commits February 16, 2026 15:47
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jared <TheWolfBadger@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* [deps]: Update minimatch to v10 [SECURITY]

* Remove erroneous failing dependencies

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Sven <svernyi@bitwarden.com>
…rate from keytar. Update .gitignore for Rust artifacts, adjust package.json for new build scripts, and modify workflows for native module compilation. Enhance state versioning to support migration of credentials from keytar to desktop_core.
…Script configurations and package dependencies. Refactor native binding logic and enhance error handling. Remove legacy keytar references and adjust package-lock.json for new native module structure.
…removing unnecessary semicolons. Update build scripts in package.json to exclude JavaScript during native module compilation.
…ule directly in the renderer process, removing the need for the credential storage listener. Update TypeScript definitions and enhance error handling in password management functions. Adjust Cargo dependencies and versions for improved compatibility.
@JaredScar JaredScar requested a review from a team as a code owner February 24, 2026 21:59
@JaredScar JaredScar added the ai-review Request a Claude code review label Feb 24, 2026
@claude
Copy link

claude bot commented Feb 24, 2026

Claude finished @JaredScar's task in 5m 0s —— View job


Code Review Summary - PR #1012: PM-12436 replace node keytar

Overview

This PR replaces the deprecated node-keytar package with a custom Rust-based native module (dc-native) that wraps desktop_core for OS keychain access. It includes a Windows-specific migration for converting credentials from keytar's UTF-8 format to desktop_core's UTF-16 format, updates the CI/CD pipeline to compile Rust instead of downloading prebuilt keytar binaries, and bumps Node from v20 to v22.

Findings

Severity Finding Location
⚠️ IMPORTANT PASSWORD_NOT_FOUND error string is hardcoded in JS and may not match the actual error message from desktop_core, causing get() to throw instead of returning null native/index.js:80
⚠️ IMPORTANT Windows credential migration only covers 6 directory-service credential keys but omits other keys (syncConfig, directoryType, organizationId, directoryConfig_) that were also stored in secure storage by the v1-to-v2 migration src/services/stateMigration.service.ts:194-201
🎨 SUGGESTED Service name "Bitwarden Directory Connector" is hardcoded in 3 separate locations; a shared constant would prevent drift electronRendererSecureStorage.service.ts:7

Additional Observations

  • No tests for new code: NativeSecureStorageService, ElectronRendererSecureStorageService refactor, and both new migration methods (migrateStateFrom3To4, migrateStateFrom4To5) have no unit tests. Given this is a credential storage migration affecting existing users, regression test coverage would reduce risk of data loss.
  • SonarQube security gate failed: The SonarQube analysis flagged a security rating issue on new code, which likely relates to the hardcoded error string comparison pattern used for credential access control flow.
  • Node version bump: The .nvmrc and engines field change from Node 20 to Node 22 is a notable change that may affect CI environments and developer setups beyond this PR's scope.

What Looks Good

  • The Rust migration logic in native/src/migration.rs correctly identifies keytar's UTF-8 format vs desktop_core's UTF-16 format using a null-byte heuristic, and the migration is idempotent.
  • Proper use of asarUnpack in electron-builder.json to ensure the native .node binary is accessible at runtime.
  • The scopeguard::defer! pattern in migration.rs ensures CredFree is called even on early returns.
  • CI pipeline changes consistently replace keytar download steps with Rust compilation across all platforms.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
E Security Rating on New Code (required ≥ D)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@github-actions
Copy link
Contributor

Logo
Checkmarx One – Scan Summary & Details9bc412fe-240f-42bf-b796-0ef63e201940

Great job! No new security vulnerabilities introduced in this pull request

isAvailable: () => nativeBinding.isAvailable(),
migrateKeytarPassword: (service, account) =>
nativeBinding.migrateKeytarPassword(service, account),
PASSWORD_NOT_FOUND: "Password not found.",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT (Correctness): PASSWORD_NOT_FOUND is hardcoded as "Password not found." in this JavaScript wrapper, but the actual error message originates from desktop_core::password functions, propagated through napi::Error::from_reason(e.to_string()) in lib.rs. If desktop_core returns a different error string (e.g., "password not found", "No password found", or a platform-specific message), the comparison e.message === passwords.PASSWORD_NOT_FOUND in both NativeSecureStorageService and ElectronRendererSecureStorageService will never match.

The consequence is that get() will throw an error instead of returning null for missing keys, and remove() will throw instead of silently succeeding. This would break credential lookups for any key not yet stored and could cause startup failures.

Consider one of:

  1. Export the constant from the Rust side so the string is defined in one place:

    #[napi]
    pub const PASSWORD_NOT_FOUND: &str = "Password not found.";

    Then reference it in index.js via nativeBinding.PASSWORD_NOT_FOUND.

  2. Use a structured error approach (error codes instead of message string comparison) so the contract is not dependent on exact text matching.

Comment on lines +194 to +201
const credentialKeys = [
SecureStorageKeys.ldap,
SecureStorageKeys.gsuite,
SecureStorageKeys.azure,
SecureStorageKeys.entra,
SecureStorageKeys.okta,
SecureStorageKeys.oneLogin,
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT (Correctness): The credential migration only covers the 6 directory-service credential keys, but the migrateStateFrom1To2 method (lines 168-178) stores all SecureStorageKeys values in secure storage with a ${userId}_ prefix. This means the following keys may also exist in the Windows credential store in the old keytar UTF-8 format and would not be migrated:

  • ${userId}_directoryConfig_ (from SecureStorageKeys.directoryConfigPrefix)
  • ${userId}_syncConfig (from SecureStorageKeys.sync)
  • ${userId}_directoryType (from SecureStorageKeys.directoryType)
  • ${userId}_organizationId (from SecureStorageKeys.organizationId)

Reading these unmigrated credentials through desktop_core on Windows would produce garbled UTF-16 output, the same corruption this migration is designed to prevent.

Suggested change
const credentialKeys = [
SecureStorageKeys.ldap,
SecureStorageKeys.gsuite,
SecureStorageKeys.azure,
SecureStorageKeys.entra,
SecureStorageKeys.okta,
SecureStorageKeys.oneLogin,
];
const credentialKeys = [
SecureStorageKeys.ldap,
SecureStorageKeys.gsuite,
SecureStorageKeys.azure,
SecureStorageKeys.entra,
SecureStorageKeys.okta,
SecureStorageKeys.oneLogin,
SecureStorageKeys.directoryConfigPrefix,
SecureStorageKeys.sync,
SecureStorageKeys.directoryType,
SecureStorageKeys.organizationId,
];


import { passwords } from "dc-native";

const APPLICATION_NAME = "Bitwarden Directory Connector";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: This service name is now hardcoded in three separate locations:

  1. Here in ElectronRendererSecureStorageService
  2. In stateMigration.service.ts line 190 (const serviceName = "Bitwarden Directory Connector")
  3. In bwdc.ts line 63 (const applicationName = "Bitwarden Directory Connector")

If any of these drift out of sync, credentials written by one path would be invisible to another. Consider extracting this to a shared constant (e.g., in a constants file) to make the coupling explicit and prevent future inconsistencies.

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 0% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 15.08%. Comparing base (984ae97) to head (216f57a).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/services/stateMigration.service.ts 0.00% 21 Missing ⚠️
src/services/nativeSecureStorage.service.ts 0.00% 16 Missing ⚠️
src/bwdc.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1012      +/-   ##
==========================================
- Coverage   15.11%   15.08%   -0.04%     
==========================================
  Files          67       67              
  Lines        2798     2804       +6     
  Branches      483      480       -3     
==========================================
  Hits          423      423              
- Misses       2271     2277       +6     
  Partials      104      104              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JaredScar JaredScar changed the base branch from main to restructure-repo February 26, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants